package br.mg.bhe.ftpobre;

import java.text.ParseException;
import java.util.Calendar;
import java.util.regex.MatchResult;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.regex.PatternSyntaxException;

import org.apache.commons.net.ftp.Configurable;
import org.apache.commons.net.ftp.FTPClientConfig;
import org.apache.commons.net.ftp.FTPFile;
import org.apache.commons.net.ftp.FTPFileEntryParserImpl;
import org.apache.commons.net.ftp.parser.FTPTimestampParser;
import org.apache.commons.net.ftp.parser.FTPTimestampParserImpl;

public class CustomUnixFTPEntryParser extends FTPFileEntryParserImpl {
	
	private static final String BEGIN_REGEX = "([bcdelfmpSs-])";
	private static final String FILE_TYPE_REGEX = "(((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-]))((r|-)(w|-)([xsStTL-])))\\+?\\s*";
	private static final String LINK_COUNT_REGEX = "(\\d+)\\s+";
	private static final String OWNER_NAME_REGEX = "(?:(\\S+(?:\\s\\S+)*?)\\s+)?";
	private static final String GROUP_NAME_REGEX = "(?:(\\S+(?:\\s\\S+)*)\\s+)?";
	private static final String SIZE_REGEX = "\\d[\\d | \\.]*(k|K|m|M|g|G|Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)\\s+";
	private static final String DATE_REGEX = "((?:\\d+[-/]\\d+[-/]\\d+)|(?:\\S{3}\\s+\\d{1,2})|(?:\\d{1,2}\\s+\\S{3}))\\s+(\\d+(?::\\d+)?)\\s+";
	private static final String END_REGEX = "(\\S*)(\\s*.*)";
	
	private static final String COMMON_REGEX = BEGIN_REGEX + FILE_TYPE_REGEX + LINK_COUNT_REGEX + OWNER_NAME_REGEX + GROUP_NAME_REGEX + END_REGEX;
	
	private static final String DEFAULT_DATE_FORMAT = "MMM d yyyy"; //Nov 9 2001

	private static final String DEFAULT_RECENT_DATE_FORMAT = "MMM d HH:mm"; //Nov 9 20:06
	
	private final FTPTimestampParser timestampParser;
	
	private Pattern pattern = null;
	private MatchResult result = null;
	protected Matcher _matcher_ = null;

	public CustomUnixFTPEntryParser() {
		this(null);
	}
	
	public CustomUnixFTPEntryParser(FTPClientConfig config) {
		this.timestampParser = new FTPTimestampParserImpl();
		configure(config);
	}

	protected FTPClientConfig getDefaultConfiguration() {
		return defaultConfiguration();
	}
	
	private String getRegexResult(String regex, String entry) {
		Matcher matcher = Pattern.compile(regex).matcher(entry);
		if (matcher.find()) {
			return matcher.group();
		} else {
			return null;
		}
	}

	@Override
	public FTPFile parseFTPEntry(String entry) {
		FTPFile file = new FTPFile();
        file.setRawListing(entry);
        int type;
        boolean isDevice = false;

        this.setRegex(COMMON_REGEX);
        if (this.matches(entry)) {
            String hardLinkCount = group(15);
            String usr = group(16);
            String grp = group(17);
            int filesize = this.getFileSizeAsBytes(this.getRegexResult(SIZE_REGEX, entry));
            String datestr = this.getRegexResult(DATE_REGEX, entry).trim();
            String name = entry.substring(entry.lastIndexOf(" ") + 1, entry.length());
            String endtoken;
            try {
            	endtoken = group(22);
            } catch (Exception ex) {
            	endtoken = null;
            }

            file.setSize(filesize);
            try
            {
                file.setTimestamp(this.parseTimestamp(datestr));
            }
            catch (ParseException e)
            {
                 // intentionally do nothing
            }

            // A 'whiteout' file is an ARTIFICIAL entry in any of several types of
            // 'translucent' filesystems, of which a 'union' filesystem is one.

            // bcdelfmpSs-
            switch (entry.charAt(0))
            {
            case 'd':
                type = FTPFile.DIRECTORY_TYPE;
                break;
            case 'e': // NET-39 => z/OS external link
                type = FTPFile.SYMBOLIC_LINK_TYPE;
                break;
            case 'l':
                type = FTPFile.SYMBOLIC_LINK_TYPE;
                break;
            case 'b':
            case 'c':
                isDevice = true;
                // break; - fall through
                //$FALL-THROUGH$ TODO change this if DEVICE_TYPE implemented
            case 'f':
            case '-':
                type = FTPFile.FILE_TYPE;
                break;
            default: // e.g. ? and w = whiteout
                type = FTPFile.UNKNOWN_TYPE;
            }

            file.setType(type);

            int g = 4;
            for (int access = 0; access < 3; access++, g += 4)
            {
                // Use != '-' to avoid having to check for suid and sticky bits
                file.setPermission(access, FTPFile.READ_PERMISSION,
                                   (!group(g).equals("-")));
                file.setPermission(access, FTPFile.WRITE_PERMISSION,
                                   (!group(g + 1).equals("-")));

                String execPerm = group(g + 2);
                if (!execPerm.equals("-") && !Character.isUpperCase(execPerm.charAt(0)))
                {
                    file.setPermission(access, FTPFile.EXECUTE_PERMISSION, true);
                }
                else
                {
                    file.setPermission(access, FTPFile.EXECUTE_PERMISSION, false);
                }
            }

            if (!isDevice)
            {
                try
                {
                    file.setHardLinkCount(Integer.parseInt(hardLinkCount));
                }
                catch (NumberFormatException e)
                {
                    // intentionally do nothing
                }
            }

            file.setUser(usr);
            file.setGroup(grp);
            
            if (null == endtoken)
            {
                file.setName(name);
            }
            else
            {
                // oddball cases like symbolic links, file names
                // with spaces in them.
                name += endtoken;
                if (type == FTPFile.SYMBOLIC_LINK_TYPE)
                {

                    int end = name.indexOf(" -> ");
                    // Give up if no link indicator is present
                    if (end == -1)
                    {
                        file.setName(name);
                    }
                    else
                    {
                        file.setName(name.substring(0, end));
                        file.setLink(name.substring(end + 4));
                    }

                }
                else
                {
                    file.setName(name);
                }
            }
            return file;
        }
        return null;
	}
	
	public void configure(FTPClientConfig config) {
        if (this.timestampParser instanceof Configurable) {
            FTPClientConfig defaultCfg = getDefaultConfiguration();
            if (config != null) {
                if (null == config.getDefaultDateFormatStr()) {
                    config.setDefaultDateFormatStr(defaultCfg.getDefaultDateFormatStr());
                }
                if (null == config.getRecentDateFormatStr()) {
                    config.setRecentDateFormatStr(defaultCfg.getRecentDateFormatStr());
                }
                ((Configurable)this.timestampParser).configure(config);
            } else {
                ((Configurable)this.timestampParser).configure(defaultCfg);
            }
        }
    }
	
	private static FTPClientConfig defaultConfiguration() {
		return new FTPClientConfig(
                FTPClientConfig.SYST_UNIX,
                DEFAULT_DATE_FORMAT,
                DEFAULT_RECENT_DATE_FORMAT,
                null, null, null);
	}
	
	private String group(int matchnum) {
        if (this.result == null) {
            return null;
        }
        return this.result.group(matchnum);
    }
	
	private Calendar parseTimestamp(String timestampStr) throws ParseException {
        return this.timestampParser.parseTimestamp(timestampStr);
    }
	
	private boolean matches(String entry) {
        this.result = null;
        _matcher_ = pattern.matcher(entry);
        if (_matcher_.matches()) {
            this.result = _matcher_.toMatchResult();
        }
        return null != this.result;
    }
	
	private void setRegex(String regex) {
        try {
            pattern = Pattern.compile(regex);
        } catch (PatternSyntaxException pse) {
            throw new IllegalArgumentException("Unparseable regex supplied: "
                    + regex);
        }
    }
	
	private int getFileSizeAsBytes(String fileSize) {
		fileSize = fileSize.toLowerCase().replaceAll("\\s(jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec)", "b").trim();
		int index = fileSize.length() - 1;
		char c = fileSize.charAt(index);
		fileSize = fileSize.substring(0, index);
		
		switch (c) {
			case 'k' :
				if (fileSize.contains(".")) {
					String[] numbers = fileSize.split("\\.");
					return (Integer.parseInt(numbers[0]) * 1024) + (Integer.parseInt(numbers[1]) * 100);
				} else {
					return (Integer.parseInt(fileSize) * 1024);
				}
			case 'm' :
				if (fileSize.contains(".")) {
					String[] numbers = fileSize.split("\\.");
					return ((Integer.parseInt(numbers[0]) * 1024) * 1024) + ((Integer.parseInt(numbers[1]) * 100) * 1024);
				} else {
					return (Integer.parseInt(fileSize) * 1024) * 1024;
				}
			case 'g' :
				if (fileSize.contains(".")) {
					String[] numbers = fileSize.split("\\.");
					return (((Integer.parseInt(numbers[0]) * 1024) * 1024) * 1024) + (((Integer.parseInt(numbers[1]) * 100) * 1024) * 1024);
				} else {
					return ((Integer.parseInt(fileSize) * 1024) * 1024) * 1024;
				}
			case 'b' : return Integer.parseInt(fileSize);
			default  : return -1;
		}
	}
}